--
-- Author: shineflag
-- Date: 2018-03-27 17:20:22
--

local skynet = require "skynet"
local socket = require "skynet.socket"
local websocket = require "websocket"
local httpd = require "http.httpd"
local urllib = require "http.url"
local sockethelper = require "http.sockethelper"
local json = require("json")
local proto = require("json_proto")
local cluster = require("skynet.cluster")
local assert = assert

local hall_conf = {
	name = "halld",
	host = "0.0.0.0",
	port = 7001
}


local log = require("log")

local server = {}

local idle_agent = {}  --空闲的agent

local ustatus = {
	online = 1,       --在线
	disconnect = 2,   --掉线
}

local meta = {}
local ws_handler = {}

local handshake = {}    --刚建立连接 尚未验证(握手成功)的连接
local users_ws = {}   --已经验证过的连接 以socket fd为key
local users = {}  --已经验证过的连接 以用户id为key

local CMD = {}

function meta.get_idle_agent()
	local agent
	if #idle_agent > 0 then 
		agent = table.remove(idle_agent,1)
	else
		agent = skynet.newservice "hall/useragent"
	end

	return agent
end


function server.shake(ws, data)
	log.d(TAG,"server.shake ws[%d] data[%s]", ws.id, data)
	local token = proto.unpack_body(data)

	local ret = 200 
	local ret_msg = "ok"
	if token and token.cmd == "hall.shake" then 

		local req = token.req 
		local mid = req.mid 
		local secret = req.secret 

		local u = users[mid] 

		if not u then 
			u = {
				mid = mid,
				secret = secret,
			}

		end

		if not u or mid == 0 then 
			ret = -101
			ret_msg = "-101 user not found"
		elseif u.secret ~= secret then 
			ret = -102 
			ret_msg = "-102 user unauthorized"
		else
			--已连接 要先断掉其它的socket
			if u.ws then 
				log.e(TAG,"user[%d] login on other ws[%d]",ws.id)
				ret_msg =  string.format("you count login on addr[%s]",ws.id)
				local pkg = proto.create_resp("hall.kick",{ret=200, msg = ret_msg})
				u.ws:send_text(pkg)
				u.ws:close()
				users_ws[u.ws.id] = nil  --不会触发 server.disconnect_handler
			end

			u.ws = ws
			users_ws[ws.id] = u
			local agent = server.shake_ok(u, secret)
			if not agent then 
				ret = -104
				ret_msg = "user not found"
			end
		end
	else
		ret = -103 
		ret_msg = "-103 error shake cmd:" .. token.cmd
	end


	local pkg = proto.create_resp("hall.shake",{ret=ret, msg = ret_msg, utime=os.time()})
	log.d(TAG,"shake response pkg[%s]",pkg)
	ws:send_text(pkg)
	if ret == 200 then 

	else
		log.e(TAG,"shake failed ret[%d] ret_msg[%s]",ret, ret_msg) 
		ws:close()
	end
end

--客户端握手成功
function server.shake_ok(u, secret)
	log.d(TAG, "shake_ok user[%d], secret[%s]",u.mid, secret)
	local agent 
	if users[u.mid] then
		log.e(TAG,"%s reconnnect", u.mid)
		agent = users[mid].agent
	else
		-- you can use a pool to alloc new agent
		agent = meta.get_idle_agent()
		local ok, flag = pcall(skynet.call,agent, "lua", "login", skynet.self(), u.mid,secret)
		if ok and flag then 
			u.agent = agent
			users[u.mid] = u
		else
			table.insert(idle_agent,agent)
			return nil 
		end
	end

	return agent
end

-- call by agent
function server.logout_handler(mid)
	log.d(TAG,"user[%d] logout and clear",mid)
	local u = users[mid]
	if u then
		users[mid] = nil
		table.insert(idle_agent, u.agent)
		--skynet.call(logind, "lua", "logout",mid)
	else
		log.d(TAG,"user[%d] not login",mid)
	end
end

-- call by login server
function server.kick_handler(mid)
	local u = users[mid]
	if u then
		-- NOTICE: logout may call skynet.exit, so you should use pcall.
		log.e(TAG,"user %d kicked",mid)
		pcall(skynet.call, u.agent, "lua", "logout")
	end
end


-- call by self (when socket disconnect)
function server.disconnect_handler(mid)
	local u = users[mid]
	if u then
		skynet.call(u.agent, "lua", "afk")
	end
end

-- call by self (when recv a request from client)
function server.request_handler(mid, msg )
	local u = users[mid]
	if u then
		if #msg ~= 20 then  --not heart beat
			log.d(TAG,"user[%d] request msg sz[%d]",mid, #msg)
		end

		local resp =  skynet.tostring(skynet.rawcall(u.agent, "client", msg))
		return resp
	else
		log.e(TAG,"user[%s] not login", mid)
	end
end



-- call by self (when gate open) 需要通知到 loginsever 供分配 
function server.register_handler(name,host, port)
	log.d(TAG,"register_handler start name[%s] host[%s] port[%d]",name, host, port)
	hall_conf.name = name 
	hall_conf.host = host 
	hall_conf.port = port
	cluster.call("cluster_web", ".logind", "register", hall_conf)
	log.d(TAG,"register_handler name[%s] host[%s] port[%d]",name, host, port)
end

function ws_handler.on_open(ws)
	log.d(TAG,"ws[%d] on_open",ws.id)
	handshake[ws.id] = ws 
	--TODO 长时间不握手则T除
end

function ws_handler.on_message(ws, msg)
	log.d(TAG,"ws[%d] recv msg[%s]", ws.id, msg)
	if handshake[ws.id] then 
		server.shake(ws,msg)
		handshake[ws.id] = nil 
	else
		local u = users_ws[ws.id]
		if u then 
			local ok, pkg = pcall(server.request_handler, u.mid, msg)
			if ok then 
				ws:send_text(pkg)
			else
				log.e(TAG,"user[%d] call info error[%s]",u.mid,pkg)
				ws:close()
			end

		else
			log.e(TAG,"not found connnection[%s]",ws.id)
			ws:close()
		end
	end
end

function ws_handler.on_close(ws, code, reason)
    log.d(TAG,"ws fd[%d] close: code[%s] rz[%s]", ws.id, code, reason)
	handshake[ws.id] = nil 
	local u = users_ws[ws.id]
	if u then 
		u.ws = nil 
		users_ws[ws.id] = nil 
		server.disconnect_handler(u.mid)
	end

end

function ws_handler.accept_socket(id, addr)
	log.d(TAG,"accept scoket[%d] addr[%s]",id, addr)
    -- limit request body size to 8192 (you can pass nil to unlimit)
    local code, url, method, header, body = httpd.read_request(sockethelper.readfunc(id), 8192)
    log.d(TAG,"code[%s], url[%s], method[%s], header[%s] body[%s]",code, url, method, json.encode(header), body)
    if code then
        
        if header.upgrade == "websocket" then
            local ws = websocket.new(id, header, ws_handler)
            ws:start()
        else
        	log.e("not a websocket fd[%d]",id)
        	socket.close(id)
        end
    else
    	log.e("not code fd[%d]",id)
    	socket.close(id)
    end
end

function CMD.open(conf)

	local address = conf.address or "0.0.0.0"
	local port = assert(conf.port)
	log.d(TAG,"Listen on %s:%d", address, port)
	local id = assert(socket.listen(address, port),"Listen error")
    socket.start(id , function(id, addr)
       socket.start(id)
       pcall(ws_handler.accept_socket, id, addr)
    end)

	local sname = assert(conf.sname)
	return server.register_handler(sname, address, port)
end

function CMD.socket_close(mid)
	local u = users[mid]
	if u and u.ws then
		u.ws:close()
	else
		log.d(TAG,"socket_close but user[%d] not login",mid)
	end
end

-- call by agent
function CMD.push(mid, data)
	local u = users[mid]
	if u and u.ws then
		u.ws:send_text(data)
		return true
	elseif not u then 
		log.e(TAG,"user[%d] not login",mid)
	else
		log.e(TAG,"user[%d] disconnect",mid)
	end
	return false
end

-- call by agent
function CMD.logout(mid)
	server.logout_handler(mid)
end



skynet.start(function()
	-- If you want to fork a work thread , you MUST do it in CMD.login
	skynet.dispatch("lua", function(session, source, command, ...)
		local f = assert(CMD[command], "not found cmd:", command)
		if session ~= 0 then
			skynet.ret(skynet.pack(f(...)))
		else
			f(... )
		end
	end)
end)
